package org.openglcharts.listeners;

import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;

import org.openglcharts.common.Value;

public abstract class Chart2DEventListener implements GLEventListener {

	public static final int ZOOM_BOTH = 0;
	public static final int ZOOM_X = 1;
	public static final int ZOOM_Y = 2;

	// domain-specific
	protected Value anchor = new Value(0.0f, 0.0f);
	protected float width = 2.0f;
	protected float height = 1.0f;
	protected float maxWidth = 0.0f;
	protected float maxHeight = 0.0f;

	// container-specific
	protected int viewportWidth = 0;
	protected int viewportHeight = 0;
	protected Value cursorPosition = new Value(0.0f, 0.0f);

	// grid-specific
	private float gridSpacingBase = 0.1f;
	private float gridSpacingX = 0.1f;
	private float gridSpacingY = 0.1f;
	private boolean gridDisplay = true;
	private boolean gridHighlightAxes = true;
	private float[] gridColorLines = { 0.3f, 0.3f, 0.3f };
	private float[] gridColorAxes = { 0.4f, 0.4f, 0.4f };
	private float[] gridColorBackground = { 0.1f, 0.1f, 0.1f };

	@Override
	public void init(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();

//		// print info
//		System.out.println("################INFO###################");
//		System.out.println("GL_VENDOR: " + gl.glGetString(GL2.GL_VENDOR));
//		System.out.println("GL_RENDERER: " + gl.glGetString(GL2.GL_RENDERER));
//		System.out.println("GL_VERSION: " + gl.glGetString(GL2.GL_VERSION));
//		System.out.println("################INFO###################");

		gl.glClearColor(this.gridColorBackground[0], this.gridColorBackground[1],
				this.gridColorBackground[2], 1.0f);

		this.initChart(drawable);
	}

	@Override
	public void display(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();

		// set projection
		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glLoadIdentity();
		gl.glOrtho(this.anchor.x, this.anchor.x + this.width, this.anchor.y,
				this.anchor.y + this.height, -1.0, 1.0);

		gl.glClear(GL2.GL_COLOR_BUFFER_BIT);

		if (this.gridDisplay)
			this.drawGrid(drawable);

		this.displayChart(drawable);
	}

	@Override
	public void dispose(GLAutoDrawable drawable) {
		// nothing to do here

		this.disposeChart(drawable);
	}

	@Override
	public void reshape(GLAutoDrawable drawable, int x, int y, int width,
			int height) {
		this.viewportWidth = drawable.getWidth();
		this.viewportHeight = drawable.getHeight();

		this.reshapeChart(drawable, x, y, width, height);
	}

	/**
	 * Sets the domain for the chart to draw on. The domain is specified by an
	 * anchor, which specifies the bottom left corner of the chart, a width and
	 * a height. The domain is not measured in pixels, but in the actual
	 * valuation of the chart.
	 * 
	 * @param anchor
	 *            the bottom-left corner of the chart
	 * @param width
	 *            the width of the chart
	 * @param height
	 *            the height of the chart
	 */
	public void setDomain(Value anchor, float width, float height) {
		this.anchor = anchor;
		this.width = width;
		this.height = height;

		this.gridSpacingX = this.calculateGridSpacing(this.width);
		this.gridSpacingY = this.calculateGridSpacing(this.height);
	}

	/**
	 * Determines whether to draw the grid-lines or not.
	 * 
	 * @param display
	 *            whether to draw the gird-lines or not.
	 */
	public void displayGrid(boolean display) {
		this.gridDisplay = display;
	}

	/**
	 * Determines whether to highlight the x- and y-axis.
	 * 
	 * @param display
	 *            whether to highlight the x- and y-axis.
	 */
	public void displayHighlightedAxes(boolean display) {
		this.gridHighlightAxes = display;
	}

	/**
	 * Sets the number of grid-lines that are to be shown in vertical, as well
	 * as horizontal directions.
	 * 
	 * @param number33333
	 *            the number of grid-lines to be shown
	 */
	public void setNumberOfGridLinesToDisplay(int number) {
		if (number != 0)
			this.gridSpacingBase = 1.0f / number;
		else
			this.gridSpacingBase = 0.0f;
	}

	/**
	 * Sets the new position of the cursor.
	 * 
	 * @param position
	 *            {@link Value} containing the new position of the cursor (in
	 *            pixels as x and y coordinates)
	 */
	public void cursorMoved(Value position) {
		this.cursorPosition = this.translateViewportCoordinate(position);
	}

	/**
	 * Moves the chart in the direction specified by the vector described by the
	 * last position of the cursor and the new one, determined by the parameters
	 * given to the method.
	 * 
	 * @param position
	 *            {@link Value} containing the position to which the cursor was
	 *            dragged (in pixels as x and y coordinates)
	 */
	public void cursorDragged(Value position) {
		Value newPosition = this.translateViewportCoordinate(position);
		float distanceX = this.cursorPosition.x - newPosition.x;
		float distanceY = this.cursorPosition.y - newPosition.y;
		this.setDomain(new Value(this.anchor.x + distanceX, this.anchor.y
				+ distanceY), this.width, this.height);
		this.cursorMoved(position);
	}

	/**
	 * Zooms in or out of the chart.<br />
	 * The parameter <i>amount</i> represents either a positive or negative
	 * integer depending on whether to widen or narrow the domain. The actual
	 * growth or reduction of the domain is relative to the domain itself and
	 * correlates to the horizontal and vertical grid-spacing.
	 * 
	 * @param amount
	 *            specifies the degree in which the domain is to be in- or
	 *            decreased
	 * @param direction
	 *            one of {@link #ZOOM_BOTH}, {@link #ZOOM_X}, {@link #ZOOM_Y}
	 */
	public void zoom(int amount, int direction) {
		float growthX = (amount * (this.width / 10.0f)) / 2;
		float growthY = (amount * (this.height / 10.0f)) / 2;

		switch (direction) {
		case ZOOM_X:
			this.setDomain(new Value(this.anchor.x - growthX, this.anchor.y),
					this.width + growthX, this.height);
			break;
		case ZOOM_Y:
			this.setDomain(new Value(this.anchor.x, this.anchor.y - growthY),
					this.width, this.height + growthY);
			break;
		default:
			this.setDomain(new Value(this.anchor.x - growthX, this.anchor.y
					- growthY), this.width + growthX, this.height + growthY);
		}
	}

	/**
	 * Translates coordinates from viewport (in pixels) to chart (relative),
	 * depending on the viewport's current resolution. Note that, while the
	 * viewport's coordinate-system has it's origin in the left upper corner,
	 * the chart's coordinate-system starts at the left lower corner. This
	 * difference is considered in the translation.
	 * 
	 * @param coordinate
	 *            {@link Value} containing the viewport-coordinates (in pixels
	 *            as x and y coordinates)
	 * @return a {@link Value} containing the translated coordinates
	 */
	private Value translateViewportCoordinate(Value coordinate) {
		float percentageX = (coordinate.x / (this.viewportWidth * 0.01f)) * 0.01f;
		float percentageY = (coordinate.y / (this.viewportHeight * 0.01f)) * 0.01f;
		return new Value(this.anchor.x + this.width * percentageX,
				(this.anchor.y + this.height) - this.height * percentageY);
	}

	/**
	 * Draws the chart's grid to passed {@link GLAutoDrawable}.<br />
	 * <br />
	 * The grid will, if possible, be drawn to fix locations so that, while
	 * moving the domain, a steady grid is achieved. To do so, the following
	 * formula is applied: <br />
	 * <br />
	 * d := beginning of domain <br />
	 * g := spacing between grid-lines <br />
	 * f(d, g) = round(s / g) * g <br />
	 * <br />
	 * Assume the grid-spacing is set to 10.0f, while the domain starts at
	 * 33.5f. A reasonable starting point would be at 30.0f, to have grid-lines
	 * at (30.0f), 40.0f, 50.0f, etc.:<br />
	 * <br />
	 * f(33.5f, 10.0f) = round(33.5f / 10.0f) * 10.0f = 30.0f
	 * 
	 * @param drawable
	 *            {@link GLAutoDrawable} to draw on
	 */
	private void drawGrid(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();

		// Calculate where the grid is to be drawn.
		float gridStartAtX = Math.round(this.anchor.x / this.gridSpacingX)
				* this.gridSpacingX;
		float gridStartAtY = Math.round(this.anchor.y / this.gridSpacingY)
				* this.gridSpacingY;
		float gridStopAtX = this.anchor.x + this.width;
		float gridStopAtY = this.anchor.y + this.height;

		// draw lines
		gl.glColor3f(this.gridColorLines[0], this.gridColorLines[1], this.gridColorLines[2]);
		gl.glBegin(GL2.GL_LINES);
		// vertical vertices
		for (float i = gridStartAtX; i <= gridStopAtX; i += this.gridSpacingX) {
			gl.glVertex2f(i, this.anchor.y);
			gl.glVertex2f(i, gridStopAtY);
		}
		// horizontal vertices
		for (float i = gridStartAtY; i <= gridStopAtY; i += this.gridSpacingY) {
			gl.glVertex2f(this.anchor.x, i);
			gl.glVertex2f(gridStopAtX, i);
		}
		// highlight axes
		if (this.gridHighlightAxes) {
			gl.glColor3f(this.gridColorAxes[0], this.gridColorAxes[1], this.gridColorAxes[2]);
			gl.glVertex2f(0, this.anchor.y);
			gl.glVertex2f(0, gridStopAtY);
			gl.glVertex2f(this.anchor.x, 0);
			gl.glVertex2f(gridStopAtX, 0);
		}

		gl.glEnd();
	}

	/**
	 * Calculates the space between two grid-lines depending on the overall
	 * space that is available. The result is based on the amount of grid-lines
	 * to draw and the domain-space to cover.
	 * 
	 * @param domainSpace
	 *            available space to draw on.
	 * @return calculated space between two grid-lines.
	 */
	private float calculateGridSpacing(float domainSpace) {
		float base = this.gridSpacingBase;
		for (; domainSpace > 1.0f; domainSpace /= 10.0f) {
			base *= 10.0f;
		}
		for (; domainSpace < 1.0f; domainSpace *= 10.0f) {
			base /= 10.0f;
		}
		return base;
	}

	protected abstract void initChart(GLAutoDrawable drawable);

	protected abstract void displayChart(GLAutoDrawable drawable);

	protected abstract void disposeChart(GLAutoDrawable drawable);

	protected abstract void reshapeChart(GLAutoDrawable drawable, int x, int y,
			int width, int height);
}
